#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import copy
import json
import bnetd
import pathlib
import asyncio
import ipaddress


class UpLinkPlugin(bnetd.UpLinkPluginBase):

    def __init__(self, plugin_id, plugin_description):
        super().__init__(plugin_id, plugin_description)

    def set_context(self, context):
        assert self._context is None and context is not None
        self._context = context

    @classmethod
    def _cfgParseFullfnAndCheck(cls, cfgPath):
        # read cfg
        cfg = None
        try:
            with open(cfgPath) as f:
                cfg = json.load(f)
        except json.JSONDecodeError:
            raise bnetd.InvalidCfgError(cfgPath, "illegal format")

        # check cfg
        if True:
            cfg2 = copy.deepcopy(cfg)

            if "uuid" in cfg2:
                del cfg2["uuid"]
            else:
                raise bnetd.InvalidCfgError(cfgPath, "missing uuid property")

            if "name" in cfg2:
                del cfg2["name"]
            else:
                raise bnetd.InvalidCfgError(cfgPath, "missing name property")

            if "description" in cfg2:
                del cfg2["description"]

            if "auto-connect" in cfg2:
                if cfg2["auto-connect"] not in ["true", "false"]:
                    raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for auto-connect property' % (cfg2["auto-connect"]))
                del cfg2["auto-connect"]

            cls.__cfgParseCheckOpenVpnPart(cfgPath, cfg2)

            if len(cfg2) > 0:
                raise bnetd.InvalidCfgError(cfgPath, "unknown property: %s" % (list(cfg2.keys())[0]))

        return cfg

    @staticmethod
    def __cfgParseCheckOpenVpnPart(cfgPath, cfg2):
        if "proto" in cfg2:
            if cfg2["proto"] not in ["udp", "tcp"]:
                raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for proto property' % (cfg2["proto"]))
            del cfg2["proto"]
        else:
            raise bnetd.InvalidCfgError(cfgPath, "missing proto property")

        if "server" in cfg2:
            del cfg2["server"]
        else:
            raise bnetd.InvalidCfgError(cfgPath, "missing server property")

        if "port" in cfg2:
            try:
                value = int(cfg2["port"])
                if value <= 0 or value >= 65536:
                    raise ValueError("out of range")
                del cfg2["port"]
            except ValueError:
                raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for port property' % (cfg2["server-port"]))

        if "inner-protocol" in cfg2:
            bGood = True
            if bGood and not isinstance(cfg2["inner-protocol"], list):
                bGood = False
            if bGood and len(cfg2["inner-protocol"]) == 0:
                bGood = False
            if bGood and len(set(cfg2["inner-protocol"])) != len(cfg2["inner-protocol"]):
                bGood = False
            if bGood and any([x not in ["ipv4", "ipv6"] for x in cfg2["inner-protocol"]]):
                bGood = False
            if not bGood:
                raise bnetd.InvalidCfgError(cfgPath, 'invalid value for inner-protocol property' % (cfg2["inner-protocol"]))
            del cfg2["inner-protocol"]
        else:
            raise bnetd.InvalidCfgError(cfgPath, "missing inner-protocol property")


class UpLink(bnetd.UpLinkBase):

    def __init__(self, plugin, src, cfg, tmpDir, dataDir):
        super().__init__(plugin, src)
        self._cfg = cfg
        self._tmpDir = tmpDir
        self._dataDir = dataDir

    def get_name(self):
        return self._cfg["name"]
    
    def get_description(self):
        return self._cfg.get("description", "")

    def get_egress_depends(self):
        if Util.isIpAddress(self._cfg["server"]):
            s = 'ip-service="%s:%d"' % (self._cfg["server"], self._plugin._cfgGetPort(self._cfg))
        else:
            s = 'domain-service="%s:%d"' % (self._cfg["server"], self._plugin._cfgGetPort(self._cfg))
        return bnetd.EgressDependency(s)

    def is_auto_connect(self):
        return self._cfg.get("auto-connect", "false") == "true"


class ObConnPlugin(bnetd.ObConnPluginBase):

    def __init__(self, plugin_id, plugin_description):
        super().__init__(plugin_id, plugin_description)
        self._context = None

    def set_context(self, context):
        assert self._context is None and context is not None
        self._context = context

    async def create_connection(self, cfg_path, cfg, stage_id, stage_context):
        assert stage_id == "out-changed"

        tmpDir = stage_context["temp-directory"]
        dataDir = stage_context["data-directory"]
        return await ObConn(cfg_path, cfg, tmpDir, dataDir)

    @staticmethod
    def _cfgParseFullfnAndCheck(cfgPath):
        # read cfg
        cfg = None
        try:
            with open(cfgPath) as f:
                cfg = json.load(f)
        except json.JSONDecodeError:
            raise bnetd.InvalidCfgError(cfgPath, "illegal format")

        # check cfg
        if True:
            cfg2 = copy.deepcopy(cfg)

            if "uuid" in cfg2:
                del cfg2["uuid"]
            else:
                raise bnetd.InvalidCfgError(cfgPath, "missing uuid property")

            if "name" in cfg2:
                del cfg2["name"]
            else:
                raise bnetd.InvalidCfgError(cfgPath, "missing name property")

            if "description" in cfg2:
                del cfg2["description"]

            if "in-gateway" in cfg2:
                if cfg2["in-gateway"] not in ["true", "false"]:
                    raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for in-gateway property' % (cfg2["in-gateway"]))
                del cfg2["in-gateway"]

            if "auto-connect" in cfg2:
                if cfg2["auto-connect"] not in ["true", "false"]:
                    raise bnetd.InvalidCfgError(cfgPath, 'invalid value "%s" for auto-connect property' % (cfg2["auto-connect"]))
                del cfg2["auto-connect"]

            UpLinkPlugin.__cfgParseCheckOpenVpnPart(cfgPath, cfg2)

            if len(cfg2) > 0:
                raise bnetd.InvalidCfgError(cfgPath, "unknown property: %s" % (list(cfg2.keys())[0]))

        return cfg

    @staticmethod
    def _cfgGetPort(cfg):
        return cfg.get("port", 1194)


class ObConn(bnetd.ObConnBase):

    def __init__(self, plugin, src, cfg, tmpDir, dataDir):
        super().__init__(plugin, src)
        self._cfg = cfg
        self._tmpDir = tmpDir
        self._dataDir = dataDir

    def get_name(self):
        return self._cfg["name"]
    
    def get_description(self):
        return self._cfg.get("description", "")

    def get_egress_depends(self):
        if Util.isIpAddress(self._cfg["server"]):
            s = 'ip-port="%s:%d"' % (self._cfg["server"], self._plugin._cfgGetPort(self._cfg))
        else:
            s = 'domain-port="%s:%d"' % (self._cfg["server"], self._plugin._cfgGetPort(self._cfg))
        return bnetd.EgressDependency(s)

    def get_egress_provides(self):
        # FIXME: should have IpService and DomainService since there's dns service
        ret = []
        if "ipv4" in self._cfg["inner-protocol"]:
            ret += [
                bnetd.EgressIpHost("0.0.0.0/0"),
            ]
        if "ipv6" in self._cfg["inner-protocol"]:
            ret += [
                bnetd.EgressIpHost("::/0"),
            ]
        ret += [
            bnetd.EgressDomainHost("."),
        ]
        return ret

    def is_in_gateway(self):
        return self._cfg.get("in-gateway", "false") == "true"

    def is_auto_connect(self):
        return self._cfg.get("auto-connect", "false") == "true"


class Util:

    @staticmethod
    def isIpAddress(s):
        try:
            ipaddress.IPv4Address(s)
            return True
        except ValueError:
            pass

        try:
            ipaddress.IPv6Address(s)
            return True
        except ValueError:
            pass

        return False





    # async def __init__(self, cfg_path, cfg, tmpDir, dataDir):
    #     super().__init__(cfg_path, cfg)

    #     buf = ""
    #     buf += "client\n"
    #     buf += "dev tun\n"
    #     buf += "proto udp\n"
    #     buf += "remote %s %s\n" % (cfg["serrver"], cfg.get("port", 1194))
    #     buf += "resolv-retry infinite\n"
    #     buf += "nobind\n"
    #     buf += "persist-key\n"
    #     buf += "persist-tun\n"

    #     cfgFullfn = os.path.join(tmpDir, "clinet.ovpn")
    #     pathlib.Path(cfgFullfn).write_text(buf)

    #     self._proc = await asyncio.create_subprocess_shell("openvpn --config %s" % (cfgFullfn))

    # async def dispose(self):
    #     self._proc.terminate()
    #     await self._proc.wait()
    #     del self._proc


# # Certificate files
# ca ca.crt
# cert client.crt
# key client.key

# # Cipher settings
# cipher AES-256-CBC
# auth SHA256

# # Additional security
# remote-cert-tls server
# tls-version-min 1.2

# # Keep connection alive
# keepalive 10 120

# # Logging
# verb 3
# mute 20



